﻿using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Web;

namespace Nonocast.Http {
	internal abstract class ServiceChannelBase : ServiceChannel {
		public virtual bool IsDoubleWay { get { return false; } }
		public NetworkStream Stream { get; set; }
		public TcpClient Client { get; set; }

		public ServiceChannelBase(ServiceRequestHeader header, SmallHTTPService host)
			: this(header) {
			this.host = host;
		}

		public ServiceChannelBase(ServiceRequestHeader header) {
			this.header = header;
		}

		public virtual void Run() {

		}

		public virtual void Close() {

		}

		public virtual void Notify(string data) {

		}

		protected void Write(string message) {
			var buffer = Encoding.UTF8.GetBytes(message);
			Write(buffer, 0, buffer.Length);
		}

		protected void Write(byte[] buffer, int offset, int size) {
			Stream.Write(buffer, offset, size);
		}

		protected ServiceRequestHeader header;
		protected SmallHTTPService host;
	}

	public class ChannelContext {
		public static ThreadLocal<ChannelContext> Contexts { get; set; }
		public static ChannelContext Current {
			get { return Contexts.Value; }
		}

		public TcpClient Client { get; set; }
		public NetworkStream Stream { get; set; }
		public string RequestUri { get { return Header.RequestUri; } }
		public string Method { get { return Header.Method; } }
		public ServiceRequestHeader Header { get; set; }
		public Arguments Arguments { get; set; }
		public int ContentLength {
			get { return Header.Properties.ContainsKey("Content-Length") ?
					 Convert.ToInt32(Header.Properties["Content-Length"]) : 0;
			}
		}
	}

	internal class StaticResourceServiceChannel : ServiceChannelBase {
		public StaticResourceServiceChannel(ServiceRequestHeader header) : base(header) { }

		public override void Run() {
			var requestFile = header.RequestUri.TrimStart('/');
			if ("favicon.ico" == requestFile) { requestFile = "Resources/" + requestFile; }
			var filepath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, HttpUtility.UrlDecode(requestFile));
			var ext = Path.GetExtension(filepath).ToLower();
			if (File.Exists(filepath) && mimes.ContainsKey(ext)) {
				var fileinfo = new FileInfo(filepath);

				string responseTemplate = "HTTP/1.1 200 OK\r\nContent-Type: {0}\r\nContent-Length: {1}\r\n\r\n";
				var responseHeader = string.Format(responseTemplate, mimes[ext], fileinfo.Length);
				Write(responseHeader);

				var buffer = new byte[4096];
				int bytesOfRead = 0;
				using (var fs = new FileStream(filepath, FileMode.Open, FileAccess.Read, FileShare.Read)) {
					while ((bytesOfRead = fs.Read(buffer, 0, 4096)) > 0) {
						Write(buffer, 0, bytesOfRead);
					}
				}
			} else {
				string message = "<h1>Not Found</h1>";
				string responseTemplate = "HTTP/1.1 404 NotFound\r\nContent-Type: text/html\r\nContent-Length: {0}\r\n\r\n{1}";
				var response = string.Format(responseTemplate, message.Length, message);
				Write(response);
			}
			Close();
		}

		static StaticResourceServiceChannel() {
			mimes[".jpg"] = "image/jpeg";
			mimes[".png"] = "image/png";
			mimes[".ico"] = "image/ico";
			mimes[".txt"] = "text/plain";
			mimes[".css"] = "text/css";
			mimes[".html"] = "text/html";
			mimes[".htm"] = "text/html";
			mimes[".mp4"] = "text/html";
			mimes[".js"] = "application/javascript";
			mimes["mp3"] = "audio/mpeg";
			mimes["mp4"] = "video/mp4";
			mimes["mpeg"] = "video/mpeg";
			mimes["mpg"] = "video/mpeg";
			mimes["zip"] = "application/zip";
			mimes["avi"] = "video/x-msvideo";
			mimes["bmp"] = "image/bmp";
			mimes["jpeg"] = "image/jpeg";
			mimes["ppt"] = "application/vnd.ms-powerpoint";
			mimes["xml"] = "application/xml";
			mimes["xls"] = "application/vnd.ms-excel";
			mimes["torrent"] = "application/octet-stream";
			mimes["flv"] = "video/x-flv";
			mimes["wma"] = "audio/x-ms-wma";
			mimes["wmv"] = "video/x-ms-wmv";
		}
		private static Dictionary<string, string> mimes = new Dictionary<string, string>();
	}

	internal class WebSocketRFC6455ServiceChannel : ServiceChannelBase {
		public override bool IsDoubleWay { get { return true; } }

		public WebSocketRFC6455ServiceChannel(ServiceRequestHeader header, SmallHTTPService host) : base(header, host) { }

		public override void Run() {
			var handshake = ComputeHandshake(header.Properties["Sec-WebSocket-Key"]);
			Console.WriteLine(handshake);

			WriteStartLine(Stream, "HTTP/1.1 101 Switching Protocols");
			WriteHeaders(Stream, new Dictionary<string, string> {
					{"Upgrade", "websocket"}, 
					{"Connection", "Upgrade"},
					{"Sec-WebSocket-Accept", handshake}});

			byte[] buffer = new byte[4096];
			int readCount = 0;
			while ((readCount = Stream.Read(buffer, 0, buffer.Length)) > 0) {
				var rcv = recData(buffer, readCount);
				if (rcv != null) {
					host.Receive(Client, rcv);
				}
			}
		}

		private string recData(byte[] recBytes, int recByteLength) {
			if (recByteLength < 2)
				return null;

			bool fin = (recBytes[0] & 0x80) == 0x80; // 1bit，1表示最后一帧  
			if (!fin) {
				Console.WriteLine("recData exception: 超过一帧"); // 超过一帧暂不处理  
				return null;
			}

			bool mask_flag = (recBytes[1] & 0x80) == 0x80; // 是否包含掩码  
			if (!mask_flag) {
				Console.WriteLine("recData exception: 没有Mask"); // 不包含掩码的暂不处理  
				return null;
			}

			int payload_len = recBytes[1] & 0x7F; // 数据长度  

			byte[] masks = new byte[4];
			byte[] payload_data;
			if (payload_len == 126) {
				Array.Copy(recBytes, 4, masks, 0, 4);
				payload_len = (UInt16)(recBytes[2] << 8 | recBytes[3]);
				payload_data = new byte[payload_len];
				Array.Copy(recBytes, 8, payload_data, 0, payload_len);
			} else if (payload_len == 127) {
				Array.Copy(recBytes, 10, masks, 0, 4);
				byte[] uInt64Bytes = new byte[8];
				for (int i = 0; i < 8; i++) {
					uInt64Bytes[i] = recBytes[9 - i];
				}
				UInt64 len = BitConverter.ToUInt64(uInt64Bytes, 0);

				payload_data = new byte[len];
				for (UInt64 i = 0; i < len; i++)
					payload_data[i] = recBytes[i + 14];
			} else {
				Array.Copy(recBytes, 2, masks, 0, 4);
				payload_data = new byte[payload_len];
				Array.Copy(recBytes, 6, payload_data, 0, payload_len);
			}

			for (var i = 0; i < payload_len; i++)
				payload_data[i] = (byte)(payload_data[i] ^ masks[i % 4]);


			return Encoding.UTF8.GetString(payload_data);
		}

		public override void Close() {
			Stream.Close();
		}

		public override void Notify(string message) {
			WriteMessage(Stream, message);
		}

		private void WriteStartLine(NetworkStream stream, string arg) {
			arg += Environment.NewLine;
			byte[] buffer = Encoding.ASCII.GetBytes(arg);
			stream.Write(buffer, 0, buffer.Length);
		}

		private void WriteHeaders(NetworkStream stream, Dictionary<string, string> headers) {
			StringBuilder sb = new StringBuilder();
			foreach (var each in headers) {
				sb.AppendLine(string.Format("{0}: {1}", each.Key, each.Value));
			}
			sb.Append(Environment.NewLine);
			byte[] buffer = Encoding.ASCII.GetBytes(sb.ToString());
			stream.Write(buffer, 0, buffer.Length);
		}

		private void WriteMessage(NetworkStream stream, string message) {
			var messageBuffer = Encoding.UTF8.GetBytes(message);
			if (messageBuffer.Length < 126) {
				stream.WriteByte(0x81);
				stream.WriteByte((byte)messageBuffer.Length);
				stream.Write(messageBuffer, 0, messageBuffer.Length);
			}
		}

		private static string ComputeHandshake(string key) {
			var challenge = key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";

			byte[] handshakeBytes = null;
			using (var sha1 = SHA1.Create()) {
				handshakeBytes = sha1.ComputeHash(Encoding.UTF8.GetBytes(challenge));
			}
			return Convert.ToBase64String(handshakeBytes);
		}
	}

	internal class WebSocketDraft76ServiceChannel : ServiceChannelBase {
		public override bool IsDoubleWay { get { return true; } }

		public WebSocketDraft76ServiceChannel(ServiceRequestHeader header, SmallHTTPService host) : base(header, host) { }

		public override void Run() {
			var handshake = ComputeHandshake(header.Properties["Sec-WebSocket-Key1"],
											 header.Properties["Sec-WebSocket-Key2"],
											 ReadKey(Stream));
			Console.WriteLine(Encoding.ASCII.GetString(handshake));

			WriteStartLine(Stream, "HTTP/1.1 101 WebSocket Protocol Handshake");
			WriteHeaders(Stream, new Dictionary<string, string> {
					{"Upgrade", "WebSocket"}, 
					{"Connection", "Upgrade"},
					{"Sec-WebSocket-Origin", header.Properties["Origin"]},
					{"Sec-WebSocket-Location", string.Format("ws://{0}{1}", header.Properties["Host"], header.RequestUri)}
				});
			WriteKey(Stream, handshake);

			byte[] buffer = new byte[4096];
			int readCount = 0;
			while ((readCount = Stream.Read(buffer, 0, buffer.Length)) > 0) {
				host.Receive(Client, Encoding.UTF8.GetString(buffer, 0, readCount));
			}
		}

		public override void Close() {
			Stream.Close();
		}

		public override void Notify(string message) {
			WriteMessage(Stream, message);
		}

		private void WriteMessage(NetworkStream stream, string message) {
			var messageBuffer = Encoding.UTF8.GetBytes(message);
			stream.WriteByte(0x00);
			stream.Write(messageBuffer, 0, messageBuffer.Length);
			stream.WriteByte(0xff);
		}

		private byte[] ReadKey(NetworkStream stream) {
			var result = new byte[8];
			stream.Read(result, 0, result.Length);
			return result;
		}

		private void WriteStartLine(NetworkStream stream, string arg) {
			arg += Environment.NewLine;
			byte[] buffer = Encoding.ASCII.GetBytes(arg);
			stream.Write(buffer, 0, buffer.Length);
		}

		private void WriteHeaders(NetworkStream stream, Dictionary<string, string> headers) {
			StringBuilder sb = new StringBuilder();
			foreach (var each in headers) {
				sb.AppendLine(string.Format("{0}: {1}", each.Key, each.Value));
			}
			sb.Append(Environment.NewLine);
			byte[] buffer = Encoding.ASCII.GetBytes(sb.ToString());
			stream.Write(buffer, 0, buffer.Length);
		}

		private void WriteKey(NetworkStream stream, byte[] handshake) {
			stream.Write(handshake, 0, handshake.Length);
		}

		private byte[] ComputeHandshake(string key1, string key2, byte[] key3) {
			byte[] result = null;
			int part1 = (int)(Convert.ToInt64(GetNumeric(key1)) / GetSpace(key1).Length);
			int part2 = (int)(Convert.ToInt64(GetNumeric(key2)) / GetSpace(key2).Length);

			var data = new List<byte>();
			data.AddRange(GetBigEndianBytes(part1));
			data.AddRange(GetBigEndianBytes(part2));
			data.AddRange(key3);

			using (var md5 = MD5.Create()) {
				result = md5.ComputeHash(data.ToArray());
			}

			return result;
		}

		private byte[] GetBigEndianBytes(int value) {
			var bytes = 4;
			var buffer = new byte[bytes];
			int num = bytes - 1;
			for (int i = 0; i < bytes; i++) {
				buffer[num - i] = (byte)(value & 0xffL);
				value = value >> 8;
			}
			return buffer;
		}

		static string GetNumeric(string arg) {
			return Regex.Replace(arg, @"\D", "");
		}

		static string GetSpace(string arg) {
			return Regex.Replace(arg, "\\S", "");
		}
	}
}
